using System;
using System.Collections.Generic;
using PostSharp.Extensibility;
using PostSharp.Laos;

namespace PostSharp4Spring
{
    /// <summary>
    /// Custom attribute that, when applied on a class, causes instances of
    /// this class to be automatically configured using 
    /// <c>ContextRegistry.GetContext().ConfigureObject</c>. 
    /// </summary>
    /// <remarks>
    /// <para>
    /// When a class is marked with the <b>[Configurable]</b> custom attribute,
    /// instances of all its derived classes will be configurable. When a parent
    /// class is marked with this custom attribute, children classes may not.
    /// In order to change the object name of children classes, use
    /// the <see cref="ObjectNameAttribute"/> custom attribute.
    /// </para>
    /// <para>
    /// The object name is determined using the following rules:
    /// </para>
    /// <list type="number">
    ///     <item><description>If the instance type is decorated with <b>[ObjectName]</b>,
    /// use this name.</description></item>
    /// <item><description>If the instance type is decorated with <b>[Configurable]</b> and
    /// a name has been specified, use this name.</description></item>
    /// <item><description>Otherwise, </description></item>
    /// </list>
    /// </remarks>
    [Serializable]
    [MulticastAttributeUsage( MulticastTargets.Class )]
    public sealed class ConfigurableAttribute : CompositionAspect
    {
        /// <summary>
        /// Object name, or <b>null</b> to use the type full name.
        /// </summary>
        private readonly string name;

        /// <summary>
        /// Cache of mappings between type names and object names.
        /// </summary>
        private static readonly Dictionary<string, string> objectNames = new Dictionary<string, string>();

        /// <summary>
        /// Type on which the custom attribute is applied.
        /// </summary>
        [NonSerialized] private Type appliedOn;

        /// <summary>
        /// Marks a class as Spring-configurable and uses the default object name
        /// (i.e. the full type name).
        /// </summary>
        public ConfigurableAttribute()
        {
        }

        /// <summary>
        /// Marks a class as Spring-configurable and specifies the object name.
        /// </summary>
        /// <param name="name"></param>
        public ConfigurableAttribute( string name )
        {
            this.name = name;
        }

        /// <summary>
        /// Perform runtime initialization of the current custom attribute instance.
        /// </summary>
        /// <param name="type">Type to which the current instance is applied.</param>
        public override void RuntimeInitialize( Type type )
        {
            base.RuntimeInitialize( type );
            this.appliedOn = type;
        }

        /// <summary>
        /// Invoked during from instance constructors to create the object
        /// implementing <see cref="IConfigurable"/>, the composed interface.
        /// We return a new instance of <seealso cref="ConfigurableImpl"/>.
        /// </summary>
        /// <param name="eventArgs">Event arguments</param>
        /// <returns>A new instance of <see cref="ConfigurableImpl"/>.</returns>
        public override object CreateImplementationObject( InstanceBoundLaosEventArgs eventArgs )
        {
            Type type = eventArgs.Instance.GetType();

            #region Get the object name.

            string assemblyQualifiedName = type.AssemblyQualifiedName;
            string name;
            if ( !objectNames.TryGetValue( assemblyQualifiedName, out name ) )
            {
                lock ( objectNames )
                {
                    if ( !objectNames.TryGetValue( assemblyQualifiedName, out name ) )
                    {
                        ObjectNameAttribute[] objectNameAttributes;
                        if (
                            ( objectNameAttributes =
                              (ObjectNameAttribute[]) type.GetCustomAttributes( typeof(ObjectNameAttribute), false ) ).
                                Length > 0 && !objectNameAttributes[0].IsDefaultAttribute() )
                        {
                            name = objectNameAttributes[0].Name;
                        }
                        else if ( type == this.appliedOn && !string.IsNullOrEmpty( this.name ) )
                        {
                            name = this.name;
                        }
                        else
                        {
                            name = type.FullName;
                        }

                        objectNames.Add( assemblyQualifiedName, name );
                    }
                }
            }

            #endregion

            return new ConfigurableImpl( eventArgs.Instance, name );
        }

        /// <summary>
        /// Invoked at compile-time to determine the interface that needs
        /// to be composed.
        /// </summary>
        /// <param name="containerType">Type to which the current custom
        /// attribute is applied.</param>
        /// <returns>Always <see cref="IConfigurable"/>.</returns>
        public override Type GetPublicInterface( Type containerType )
        {
            return typeof(IConfigurable);
        }
    }
}